package com.seven.service;

import com.alibaba.excel.EasyExcel;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.seven.dao.TicketDao;
import com.seven.domain.Ticket;
import com.seven.domain.TicketDownloadVO;
import com.seven.util.CsvUtil;
import com.seven.util.HeaderUtil;
import com.seven.util.InvoiceTypeUtil;
import java.math.BigDecimal;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.assertj.core.util.Lists;
import org.assertj.core.util.Strings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author seven
 */
@Service
@Slf4j
public class UploadService {

  @Value("${invoice.year:2021}")
  private String invoiceYear;

  @Value("${invoice.tax.code}")
  private String taxCode;

  @Value("${invoice.tax.address}")
  private String address;

  @Value("${invoice.tax.company}")
  private String companyName;

  @Value("${invoice.tax.phone}")
  private String phone;

  @Value("${invoice.base.path:d:/opt/data/invoice/2020-09/}")
  private String basePath;

  @Value("${invoice.regex:^(¥|￥)(-?[0-9,]+)(\\.[0-9]+)?}")
  private String regex;

  @Value("${invoice.tax.download:true}")
  private Boolean downloadFailed;

  public static Pattern regexPattern;

  @Autowired
  private TicketDao ticketDao;

  private static Pattern pattern = Pattern.compile("[^0-9]");

  @PostConstruct
  public void init() {
    File folder = new File(basePath.substring(0, basePath.length() - 1));
    if (!folder.exists() || !folder.isDirectory()) {
      boolean mkdirs = folder.mkdirs();
      if (mkdirs) {
        log.info("成功创建文件夹{}", basePath);
      } else {
        log.info("失败创建文件夹{}", basePath);
      }
    } else {
      log.info("文件夹: {}已存在", basePath);
    }
    regexPattern = Pattern.compile(regex);
    log.info("金额正则表达式已启用：{}", regex);
  }

  public String upload(List<MultipartFile> multipartFiles, String userName, String userCode,
      String groupName) {

    List<Ticket> ticketList = new ArrayList<>();
    String token = HeaderUtil.getToken();

    if (!CollectionUtils.isEmpty(multipartFiles)) {
      for (MultipartFile multipartFile : multipartFiles) {
        //init ticket
        Ticket t = new Ticket();
        t.setId(UUID.randomUUID().toString().substring(26));
        t.setCreatedAt(new Date());
        t.setDeleted(false);
        t.setSuccess(true);
        t.setToken(token);
        t.setFileName(multipartFile.getOriginalFilename());
        t.setUserName(userName);
        t.setUserCode(userCode);
        t.setGroupName(groupName);

        if (Objects.requireNonNull(multipartFile.getOriginalFilename()).endsWith(".pdf")) {
          process(multipartFile, t);
        } else {
          t.setSuccess(false);
          t.setMessage("文件名称后缀不是PDF");
        }
        ticketList.add(t);
      }
    }

    log.info("完成发票的上传和名称修改");
    ticketList.sort(Comparator.comparing(Ticket::getSuccess));

    log.info("保留执行成功的发票信息");
    ticketList.forEach(ticket -> ticketDao.insert(ticket));

    long total = 0;
    long success = 0;
    long failed = 0;
    if (!CollectionUtils.isEmpty(ticketList)) {
      total = ticketList.size();
      success = ticketList.stream().filter(Ticket::getSuccess).count();
      failed = total - success;
    }

    return String.format("共识别%d张发票，其中成功%d张，失败%d张", total, success, failed);
  }

  /**
   * 1，pdf文件解析
   * 2，pdf文件保存
   * 3，pdf文件修改名称
   *
   * @param multipartFile 上传文件
   */
  private void process(MultipartFile multipartFile, Ticket ticket) {

    //优先保存发票文件，再做处理
    String baseDir = basePath + ticket.getToken();
    File baseDirFile = new File(baseDir);
    if (!baseDirFile.exists()) {
      if (baseDirFile.mkdir()) {
        log.info("{}路径创建完成", baseDir);
      } else {
        log.info("{}路径创建失败", baseDir);
      }
    }
    String path = baseDir + File.separator + multipartFile.getOriginalFilename();
    File saveFile = new File(path);
    try {
      multipartFile.transferTo(saveFile);
      log.info("保存文件完成{},token{}", saveFile, ticket.getToken());
    } catch (IOException e) {
      ticket.setSuccess(false);
      ticket.setMessage("保存文件失败，建议再次尝试");
      log.error("保存文件失败: {}", e.getMessage());
      return;
    }

    //解析会初始化ticket是否成功，成功解析就会设置true，解析失败就会设置false
    pdfParseAndCheck(saveFile, ticket);

    //识别出发票代码和发票号码之后，才修改名字
    if (!Strings.isNullOrEmpty(ticket.getCode()) && !Strings.isNullOrEmpty(ticket.getNumber())) {
      modifyFileName(path, ticket);
    }
  }

  private void pdfParseAndCheck(File saveFile, Ticket ticket) {
    Invoice invoice;
    try {
      invoice = InvoiceExtractor.extract(saveFile);
    } catch (Exception e) {
      log.error("识别失败", e);
      ticket.setSuccess(false);
      ticket.setMessage(e.getMessage());
      return;
    }

    //1,初始化ticket
    ticket.setCode(invoice.getCode());
    ticket.setNumber(invoice.getNumber());
    ticket.setDate(invoice.getDate());
    String checksum = invoice.getChecksum();
    if (checksum != null && checksum.length() > 6) {
      //取校验码后六位
      ticket.setCheckNum(checksum.substring(checksum.length() - 6));
    }
    ticket.setTotal(invoice.getTotalAmount());
    ticket.setTaxCode(invoice.getBuyerCode());

    //识别发票代码
    String invoiceName = invoice.getDetailList().stream().map(Detail::getName)
        .collect(Collectors.toSet()).toString();
    String invoiceType = InvoiceTypeUtil.getInvoiceType(invoiceName);
    ticket.setTaxTypeCode(invoiceType);

    //识别页码
    ticket.setNumberOfPages(invoice.getNumberOfPages());

    //2,校验
    //发票类别校验
    if (invoiceType == null) {
      ticket.setSuccess(false);
      ticket.setMessage("发票类型不支持，当前仅支持汽油票、客运服务费以及通行费3种，请确认");
      return;
    }

    //校验是否包含充值、预付、柴油
    if (invoiceName.contains("充值")) {
      ticket.setSuccess(false);
      ticket.setMessage("带充值字样的发票无法报销");
      return;
    }
    if (invoiceName.contains("预付")) {
      ticket.setSuccess(false);
      ticket.setMessage("带预付字样的发票无法报销");
      return;
    }
    if (invoiceName.contains("柴油")) {
      ticket.setSuccess(false);
      ticket.setMessage("带柴油字样的发票无法报销");
      return;
    }
    //TODO 通行费校验通行时间


    //校验公司名称中移（苏州）软件技术有限公司
    if (!companyName.equals(invoice.getBuyerName())) {
      ticket.setSuccess(false);
      ticket.setMessage("公司名称错误");
      return;
    }

    //校验税号
    if (!taxCode.equals(ticket.getTaxCode())) {
      ticket.setSuccess(false);
      ticket.setMessage("纳税人识别号错误");
      return;
    }

    //校验电话号码和地址
    String buyerAddress = invoice.getBuyerAddress();
    if (!Strings.isNullOrEmpty(buyerAddress)) {
      if (!buyerAddress.contains(address) || !buyerAddress.contains(phone)) {
        ticket.setSuccess(false);
        ticket.setMessage("地址、电话填写错误");
        return;
      }
    }

    //校验金额是否超过500元
    BigDecimal total = ticket.getTotal();
    if (total != null) {
      if (total.compareTo(new BigDecimal("500.0")) > 0) {
        ticket.setSuccess(false);
        ticket.setMessage("发票号码超过500元");
        return;
      }
    }

    //校验开票日期必须在4个月以内
    String date = ticket.getDate();
    if (!Strings.isNullOrEmpty(date)) {
      date = formatDateYearMonthDate(date);
      //更新为解析后的格式
      ticket.setDate(date);
      YearMonth now = YearMonth.now();
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy/MM/dd");
      YearMonth invoiceCreateYearMonth = YearMonth.parse(date, dtf);
      if (!now.minusMonths(4).isBefore(invoiceCreateYearMonth)) {
        ticket.setSuccess(false);
        ticket.setMessage("只能报销3个月的发票");
        return;
      }
    }

    ticket.setMessage("无");
  }

  private String formatDateYearMonthDate(String date) {
    //至少8为如20210321
    //正常情况下为2021年03月21日
    //遇到过识别失败的为20210321日
    if (date.length() > 7) {
      if (date.contains("年")) {
        date = date.replaceAll("年", "/");
      } else {
        date = date.substring(0, 4) + "/" + date.substring(4);
      }
      if (date.contains("月")) {
        date = date.replaceAll("月", "/");
      } else {
        date = date.substring(0, 7) + "/" + date.substring(7);
      }
      if (date.contains("日")) {
        date = date.replaceAll("日", "");
      }
    }
    return date;
  }


  /**
   * 修改文件名称
   * @param path
   * @param ticket
   */
  private void modifyFileName(String path, Ticket ticket) {
    File file = new File(path);
    String subPath = path.substring(0,path.lastIndexOf(File.separator));
    String newFileName = ticket.getCode() + "+" + ticket.getNumber() + "+" + ticket.getUserName() + ".pdf";

    File newFile = new File(subPath + File.separator + newFileName);
    try {
      if (newFile.exists()) {
        log.info("修改文件名失败，文件路径 {}", subPath + File.separator + newFileName);
        ticket.setSuccess(false);
        ticket.setMessage("指定格式的发票文件已经存在，请不要重复操作");
        return;
      }
      if (file.renameTo(newFile)) {
        //保存新的文件名
        ticket.setFileName(newFileName);
        return;
      }
    } catch (Exception e) {
      log.error("修改文件名称失败：{}",e.getMessage());
    }
    ticket.setSuccess(false);
    ticket.setMessage("修改名称失败，建议联系管理员");
  }

  /**
   * 解析PDF文件
   */
  private void pdfParse(File multipartFile, Ticket ticket) {
    String code, number, date, checkNum, total;
    try (InputStream input = new FileInputStream(multipartFile);
         PDDocument document = PDDocument.load(input)) {

      PDFTextStripper stripper = new PDFTextStripper();
      String text = stripper.getText(document);
      log.info("发票信息：{}", ticket);
      log.info("origin text: {}", text);

      String lineSeparator = System.getProperty("line.separator");
      List<String> split = Arrays.asList(text.split(lineSeparator));
      if (!split.contains(taxCode)) {
        ticket.setMessage("发票税号不匹配");
      } else {
        for (int i = 0; i < split.size(); i++) {
          if (split.get(i).length() == 12) {
            if (split.get(i + 1).length() == 8) {
              if (split.get(i + 2).startsWith(invoiceYear)) {
                String[] checkNumList = split.get(i + 3).split(" ");
                StringBuilder builder = new StringBuilder();
                for (String s : checkNumList) {
                  builder.append(s.trim());
                }
                checkNum = builder.toString();
                if (checkNum.length() == 20) {
                  code = split.get(i);
                  number = split.get(i + 1);
                  String totalLine = split.get(i + 4);
                  Matcher matcher = regexPattern.matcher(totalLine);
                  if (matcher.find()) {
                    total = matcher.group().substring(1);
                  } else {
                    total = getTotal(split);
                  }

                  date = pattern.matcher(split.get(i + 2)).replaceAll("");
                  date = formatDate(date);

                  ticket.setCode(code);
                  ticket.setNumber(number);
                  ticket.setCheckNum(checkNum.substring(checkNum.length() - 6));
                  ticket.setDate(date);
                  ticket.setTaxCode(taxCode);
                  ticket.setSuccess(true);
                  ticket.setTotal(new BigDecimal(total));
                  return;
                }
              }
            }
          }
        }
      }
    } catch (Exception e) {
      log.error(e.getMessage());
    }
    ticket.setSuccess(false);
    if (Strings.isNullOrEmpty(ticket.getMessage())) {
      ticket.setMessage("无法识别PDF中的发票信息，建议手动填写发票信息");
    }
  }

  /**
   * 找到最大值，即为发票
   */
  private String getTotal(List<String> split) {
    List<String> costList = new ArrayList<>();
    //匹配金额
    for (String s : split) {
      //¥255.42 ¥33.20
      //¥288.62贰佰捌拾捌圆陆角贰分
      Matcher matcher = regexPattern.matcher(s);
      while (matcher.find()) {
        costList.add(matcher.group().substring(1));
      }
    }

    //求最大值
    double maxTotal = -1;
    for (String cost : costList) {
      double tmpCost = Double.parseDouble(cost.trim());
      if (tmpCost > maxTotal) {
        maxTotal = tmpCost;
      }
    }
    return maxTotal == -1 ? null : maxTotal + "";
  }

  /**
   * 日期信息格式化
   * @param date
   * @return
   */
  private String formatDate(String date) {
    if (date.length() == 8) {
      return date.substring(0, 4) + "/" + date.substring(4, 6) + "/" + date.substring(6);
    }

    if (Integer.getInteger(date.substring(4, 5)).equals(0)) {
      return date.substring(0, 4) + "/" + date.substring(4, 6) + "/" + date.substring(6);
    }

    return date.substring(0, 4) + "/" + date.substring(4, 5) + "/" + date.substring(5);
  }


  public IPage<Ticket> list(Integer page, Integer size, String userName, String groupName,
                            String number, String orderBy, String orderDir) {

    OrderItem orderItem;
    if (orderBy == null) {
      orderBy = "created_at";
    }
    if ("ASC".equalsIgnoreCase(orderDir)) {
      orderItem = OrderItem.asc(orderBy);
    } else {
      orderItem = OrderItem.desc(orderBy);
    }
    Page<Ticket> pageParam = Page.of(page, size);
    pageParam.addOrder(Lists.newArrayList(orderItem));
    return ticketDao.selectPage(pageParam, userName);
  }

  public List<Ticket> csvExport(String userName, String groupName, String number, HttpServletResponse response) throws IOException {
    List<Ticket> tickets = ticketDao.selectList(userName, groupName, number);
    if (CollectionUtils.isEmpty(tickets)) {
      return tickets;
    }

    List<Map<String, Object>> dataList = new ArrayList<>();
    Map<String, Object> map;
    for (Ticket t : tickets) {
      map = new HashMap<>(7);
      map.put("code", t.getCode());
      map.put("number", t.getNumber());
      map.put("date", t.getDate());
      map.put("checkNum", t.getCheckNum());
      map.put("userName", t.getUserName());
      map.put("groupName", t.getGroupName());
      dataList.add(map);
    }
    String titles = "发票代码,发票号码,开票日期,校验码后六位,姓名,组名称";
    String keys = "code,number,date,checkNum,userName,groupName";
    ServletOutputStream outputStream = response.getOutputStream();
    CsvUtil.responseSetProperties("", response);
    CsvUtil.doExport(dataList, titles, keys, outputStream);
    return tickets;
  }

  public void delete(List<String> idList) {
    if (CollectionUtils.isEmpty(idList)) {
      log.info("无需要删除的发票文件");
      return;
    }
    for (String id : idList) {
      Ticket ticket = ticketDao.selectById(id);
      ticketDao.softDeleteById(id);
      if (!Strings.isNullOrEmpty(ticket.getFileName())) {
        File file = new File(basePath + ticket.getToken() + File.separator + ticket.getFileName());
        removeFile(file);
      }
    }
  }

  public String download(List<String> idList, HttpServletResponse response) throws IOException {
    StringBuilder builder = new StringBuilder();
    //生成zip文件存放位置
    String encodeFileNamePrefix = URLEncoder.encode("压缩后的pdf发票文件-", "utf-8");
    String zipFileName = encodeFileNamePrefix + System.currentTimeMillis() + ".zip";
    String strZipPath = basePath + zipFileName;
    File file = new File(strZipPath);

    //告诉浏览器输出内容为流
    response.setContentType("application/octet-stream");
    //Content-Disposition中指定的类型是文件的扩展名，并且弹出的下载对话框中的文件类型图片是按照文件的扩展名显示的，点保存后，文件以filename的值命名，保存类型以Content中设置的为准。注意：在设置Content-Disposition头字段之前，一定要设置Content-Type头字段。
    response.addHeader("Content-Disposition", "attachment;filename="
        + zipFileName);

    //通过response的outputStream输出文件
    ServletOutputStream outputStream = response.getOutputStream();
    ZipOutputStream out = new ZipOutputStream(new FileOutputStream(strZipPath));
    for (String id : idList) {
      Ticket ticket = ticketDao.selectById(id);
      if (!downloadFailed) {
        if (ticket == null || Objects.equals(ticket.getSuccess(), false)) {
          log.warn("发票不存在，或者发票未识别成功");
          continue;
        }
      }
      String fileName = ticket.getFileName();
      if (!Strings.isNullOrEmpty(fileName)) {
        try {
          File pdfFile = new File(basePath + ticket.getToken() + File.separator + fileName);
          if (pdfFile.exists()) {
            out.putNextEntry(new ZipEntry(fileName));
            // 读入需要下载的文件的内容，打包到zip文件
            out.write(FileCopyUtils.copyToByteArray(pdfFile));
            out.closeEntry();
          } else {
            log.info("未识别的发票无法下载，文件名：{}", fileName);
          }
        } catch (IOException e) {
          log.info("pdf文件写入Zip文件失败", e);
          builder.append(fileName).append(",");
        }
      } else {
        builder.append(fileName).append(",");
      }
    }
    out.close();
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream(strZipPath));
    //将输入流的数据拷贝到输入流输出
    FileCopyUtils.copy(bis, outputStream);
    log.info("删除zip文件{}", file.getName());
    removeFile(file);

    if (builder.length() > 0) {
      log.info("存在无法下载的文件: {}", builder.toString());
      builder.append("无法下载");
    }
    return builder.toString();
  }

  private void removeFile(File file) {
    if (file.exists()) {
      if (file.delete()) {
        log.info("文件{}删除成功", file.getName());
      } else {
        log.info("文件{}删除失败", file.getName());
      }
    }
  }

  public void downloadExcel(List<String> idList, HttpServletResponse response) throws IOException {
    List<Ticket> tickets = ticketDao.selectBatchIds(idList);
    if (!downloadFailed) {
      if (!CollectionUtils.isEmpty(tickets)) {
        //过滤识别成功的发票
        tickets = tickets.stream().filter(Ticket::getSuccess).collect(Collectors.toList());
      }
    }
    // 设置文本内省
    response.setContentType("application/vnd.ms-excel");
    // 设置字符编码
    response.setCharacterEncoding("utf-8");
    String fileNameEncoded = URLEncoder.encode("增值税普票导入模板", "utf-8");
    // 设置响应头
    response.setHeader("Content-disposition",
        "attachment;filename=" + fileNameEncoded + System.currentTimeMillis() + ".xlsx");
    //用io流来写入数据
    EasyExcel.write(response.getOutputStream(), TicketDownloadVO.class).sheet("增值税普票")
        .doWrite(tickets);
  }

}
